/*
 * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/multipart/Part.java,v 1.16 2005/01/14 21:16:40 olegk Exp $
 * $Revision: 480424 $
 * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
 *
 * ====================================================================
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package code.bigmama.http.multipart;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import org.apache.http.util.EncodingUtils;
import org.apache.commons.logging.Log;

//import org.apache.commons.logging.LogFactory;

/**
 * Abstract class for one Part of a multipart post object.
 * 
 * @author <a href="mailto:mattalbright@yahoo.com">Matthew Albright</a>
 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
 * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a>
 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
 * 
 * @since 2.0
 */
public abstract class Part
{

	/** Log object for this class. */
	// private static final Log LOG = LogFactory.getLog(Part.class);

	/**
	 * The boundary
	 * 
	 * @deprecated use
	 *             {@link org.apache.http.client.methods.multipart#MULTIPART_BOUNDARY}
	 */
	protected static final String	BOUNDARY						= "----------------314159265358979323846";

	/**
	 * The boundary as a byte array.
	 * 
	 * @deprecated
	 */
	protected static final byte[]	BOUNDARY_BYTES					= EncodingUtils.getAsciiBytes(BOUNDARY);

	/**
	 * The default boundary to be used if {@link #setPartBoundary(byte[])} has
	 * not been called.
	 */
	private static final byte[]		DEFAULT_BOUNDARY_BYTES			= BOUNDARY_BYTES;

	/** Carriage return/linefeed */
	protected static final String	CRLF							= "\r\n";

	/** Carriage return/linefeed as a byte array */
	protected static final byte[]	CRLF_BYTES						= EncodingUtils.getAsciiBytes(CRLF);

	/** Content dispostion characters */
	protected static final String	QUOTE							= "\"";

	/** Content dispostion as a byte array */
	protected static final byte[]	QUOTE_BYTES						= EncodingUtils.getAsciiBytes(QUOTE);

	/** Extra characters */
	protected static final String	EXTRA							= "--";

	/** Extra characters as a byte array */
	protected static final byte[]	EXTRA_BYTES						= EncodingUtils.getAsciiBytes(EXTRA);

	/** Content dispostion characters */
	protected static final String	CONTENT_DISPOSITION				= "Content-Disposition: form-data; name=";

	/** Content dispostion as a byte array */
	protected static final byte[]	CONTENT_DISPOSITION_BYTES		= EncodingUtils.getAsciiBytes(CONTENT_DISPOSITION);

	/** Content type header */
	protected static final String	CONTENT_TYPE					= "Content-Type: ";

	/** Content type header as a byte array */
	protected static final byte[]	CONTENT_TYPE_BYTES				= EncodingUtils.getAsciiBytes(CONTENT_TYPE);

	/** Content charset */
	protected static final String	CHARSET							= "; charset=";

	/** Content charset as a byte array */
	protected static final byte[]	CHARSET_BYTES					= EncodingUtils.getAsciiBytes(CHARSET);

	/** Content type header */
	protected static final String	CONTENT_TRANSFER_ENCODING		= "Content-Transfer-Encoding: ";

	/** Content type header as a byte array */
	protected static final byte[]	CONTENT_TRANSFER_ENCODING_BYTES	= EncodingUtils.getAsciiBytes(CONTENT_TRANSFER_ENCODING);

	/**
	 * Return the boundary string.
	 * 
	 * @return the boundary string
	 * @deprecated uses a constant string. Rather use {@link #getPartBoundary}
	 */
	public static String getBoundary()
	{
		return BOUNDARY;
	}

	/**
	 * The ASCII bytes to use as the multipart boundary.
	 */
	private byte[]	boundaryBytes;

	/**
	 * Return the name of this part.
	 * 
	 * @return The name.
	 */
	public abstract String getName();

	/**
	 * Returns the content type of this part.
	 * 
	 * @return the content type, or <code>null</code> to exclude the content
	 *         type header
	 */
	public abstract String getContentType();

	/**
	 * Return the character encoding of this part.
	 * 
	 * @return the character encoding, or <code>null</code> to exclude the
	 *         character encoding header
	 */
	public abstract String getCharSet();

	/**
	 * Return the transfer encoding of this part.
	 * 
	 * @return the transfer encoding, or <code>null</code> to exclude the
	 *         transfer encoding header
	 */
	public abstract String getTransferEncoding();

	/**
	 * Gets the part boundary to be used.
	 * 
	 * @return the part boundary as an array of bytes.
	 * 
	 * @since 3.0
	 */
	protected byte[] getPartBoundary()
	{
		if (boundaryBytes == null)
		{
			// custom boundary bytes have not been set, use the default.
			return DEFAULT_BOUNDARY_BYTES;
		}
		else
		{
			return boundaryBytes;
		}
	}

	/**
	 * Sets the part boundary. Only meant to be used by
	 * {@link Part#sendParts(OutputStream, Part[], byte[])} and
	 * {@link Part#getLengthOfParts(Part[], byte[])}
	 * 
	 * @param boundaryBytes
	 *            An array of ASCII bytes.
	 * @since 3.0
	 */
	void setPartBoundary(byte[] boundaryBytes)
	{
		this.boundaryBytes = boundaryBytes;
	}

	/**
	 * Tests if this part can be sent more than once.
	 * 
	 * @return <code>true</code> if {@link #sendData(OutputStream)} can be
	 *         successfully called more than once.
	 * @since 3.0
	 */
	public boolean isRepeatable()
	{
		return true;
	}

	/**
	 * Write the start to the specified output stream
	 * 
	 * @param out
	 *            The output stream
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	protected void sendStart(OutputStream out) throws IOException
	{
		// LOG.trace("enter sendStart(OutputStream out)");
		out.write(EXTRA_BYTES);
		out.write(getPartBoundary());
		out.write(CRLF_BYTES);
	}

	/**
	 * Write the content disposition header to the specified output stream
	 * 
	 * @param out
	 *            The output stream
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	protected void sendDispositionHeader(OutputStream out) throws IOException
	{
		// LOG.trace("enter sendDispositionHeader(OutputStream out)");
		out.write(CONTENT_DISPOSITION_BYTES);
		out.write(QUOTE_BYTES);
		out.write(EncodingUtils.getAsciiBytes(getName()));
		out.write(QUOTE_BYTES);
	}

	/**
	 * Write the content type header to the specified output stream
	 * 
	 * @param out
	 *            The output stream
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	protected void sendContentTypeHeader(OutputStream out) throws IOException
	{
		// LOG.trace("enter sendContentTypeHeader(OutputStream out)");
		String contentType = getContentType();
		if (contentType != null)
		{
			out.write(CRLF_BYTES);
			out.write(CONTENT_TYPE_BYTES);
			out.write(EncodingUtils.getAsciiBytes(contentType));
			String charSet = getCharSet();
			if (charSet != null)
			{
				out.write(CHARSET_BYTES);
				out.write(EncodingUtils.getAsciiBytes(charSet));
			}
		}
	}

	/**
	 * Write the content transfer encoding header to the specified output stream
	 * 
	 * @param out
	 *            The output stream
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	protected void sendTransferEncodingHeader(OutputStream out) throws IOException
	{
		// LOG.trace("enter sendTransferEncodingHeader(OutputStream out)");
		String transferEncoding = getTransferEncoding();
		if (transferEncoding != null)
		{
			out.write(CRLF_BYTES);
			out.write(CONTENT_TRANSFER_ENCODING_BYTES);
			out.write(EncodingUtils.getAsciiBytes(transferEncoding));
		}
	}

	/**
	 * Write the end of the header to the output stream
	 * 
	 * @param out
	 *            The output stream
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	protected void sendEndOfHeader(OutputStream out) throws IOException
	{
		// LOG.trace("enter sendEndOfHeader(OutputStream out)");
		out.write(CRLF_BYTES);
		out.write(CRLF_BYTES);
	}

	/**
	 * Write the data to the specified output stream
	 * 
	 * @param out
	 *            The output stream
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	protected abstract void sendData(OutputStream out) throws IOException;

	/**
	 * Return the length of the main content
	 * 
	 * @return long The length.
	 * @throws IOException
	 *             If an IO problem occurs
	 */
	protected abstract long lengthOfData() throws IOException;

	/**
	 * Write the end data to the output stream.
	 * 
	 * @param out
	 *            The output stream
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	protected void sendEnd(OutputStream out) throws IOException
	{
		// LOG.trace("enter sendEnd(OutputStream out)");
		out.write(CRLF_BYTES);
	}

	/**
	 * Write all the data to the output stream. If you override this method make
	 * sure to override #length() as well
	 * 
	 * @param out
	 *            The output stream
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	public void send(OutputStream out) throws IOException
	{
		// LOG.trace("enter send(OutputStream out)");
		sendStart(out);
		sendDispositionHeader(out);
		sendContentTypeHeader(out);
		sendTransferEncodingHeader(out);
		sendEndOfHeader(out);
		sendData(out);
		sendEnd(out);
	}

	/**
	 * Return the full length of all the data. If you override this method make
	 * sure to override #send(OutputStream) as well
	 * 
	 * @return long The length.
	 * @throws IOException
	 *             If an IO problem occurs
	 */
	public long length() throws IOException
	{
		// LOG.trace("enter length()");
		if (lengthOfData() < 0)
		{
			return -1;
		}
		ByteArrayOutputStream overhead = new ByteArrayOutputStream();
		sendStart(overhead);
		sendDispositionHeader(overhead);
		sendContentTypeHeader(overhead);
		sendTransferEncodingHeader(overhead);
		sendEndOfHeader(overhead);
		sendEnd(overhead);
		return overhead.size() + lengthOfData();
	}

	/**
	 * Return a string representation of this object.
	 * 
	 * @return A string representation of this object.
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString()
	{
		return this.getName();
	}

	/**
	 * Write all parts and the last boundary to the specified output stream.
	 * 
	 * @param out
	 *            The stream to write to.
	 * @param parts
	 *            The parts to write.
	 * 
	 * @throws IOException
	 *             If an I/O error occurs while writing the parts.
	 */
	public static void sendParts(OutputStream out, final Part[] parts) throws IOException
	{
		sendParts(out, parts, DEFAULT_BOUNDARY_BYTES);
	}

	/**
	 * Write all parts and the last boundary to the specified output stream.
	 * 
	 * @param out
	 *            The stream to write to.
	 * @param parts
	 *            The parts to write.
	 * @param partBoundary
	 *            The ASCII bytes to use as the part boundary.
	 * 
	 * @throws IOException
	 *             If an I/O error occurs while writing the parts.
	 * 
	 * @since 3.0
	 */
	public static void sendParts(OutputStream out, Part[] parts, byte[] partBoundary) throws IOException
	{

		if (parts == null)
		{
			throw new IllegalArgumentException("Parts may not be null");
		}
		if (partBoundary == null || partBoundary.length == 0)
		{
			throw new IllegalArgumentException("partBoundary may not be empty");
		}
		for (int i = 0; i < parts.length; i++)
		{
			// set the part boundary before the part is sent
			parts[i].setPartBoundary(partBoundary);
			parts[i].send(out);
		}
		out.write(EXTRA_BYTES);
		out.write(partBoundary);
		out.write(EXTRA_BYTES);
		out.write(CRLF_BYTES);
	}

	/**
	 * Return the total sum of all parts and that of the last boundary
	 * 
	 * @param parts
	 *            The parts.
	 * @return The total length
	 * 
	 * @throws IOException
	 *             If an I/O error occurs while writing the parts.
	 */
	public static long getLengthOfParts(Part[] parts) throws IOException
	{
		return getLengthOfParts(parts, DEFAULT_BOUNDARY_BYTES);
	}

	/**
	 * Gets the length of the multipart message including the given parts.
	 * 
	 * @param parts
	 *            The parts.
	 * @param partBoundary
	 *            The ASCII bytes to use as the part boundary.
	 * @return The total length
	 * 
	 * @throws IOException
	 *             If an I/O error occurs while writing the parts.
	 * 
	 * @since 3.0
	 */
	public static long getLengthOfParts(Part[] parts, byte[] partBoundary) throws IOException
	{
		// LOG.trace("getLengthOfParts(Parts[])");
		if (parts == null)
		{
			throw new IllegalArgumentException("Parts may not be null");
		}
		long total = 0;
		for (int i = 0; i < parts.length; i++)
		{
			// set the part boundary before we calculate the part's length
			parts[i].setPartBoundary(partBoundary);
			long l = parts[i].length();
			if (l < 0)
			{
				return -1;
			}
			total += l;
		}
		total += EXTRA_BYTES.length;
		total += partBoundary.length;
		total += EXTRA_BYTES.length;
		total += CRLF_BYTES.length;
		return total;
	}
}
